//
//  LLBasicModel.m
//  LLFutureLife
//
//  Created by Tomy on 13-7-9.
//  Copyright (c) 2013年 ZhiHai. All rights reserved.
//

#import "LLBasicModel.h"
#import "JSONValueTransformer.h"

#pragma mark - class static variables
static NSArray* allowedJSONTypes = nil;
static NSArray* allowedPrimitiveTypes = nil;
static JSONValueTransformer* valueTransformer = nil;

#pragma mark - model cache
static NSMutableDictionary* classProperties = nil;
static NSMutableDictionary* classRequiredPropertyNames = nil;
static NSMutableDictionary* classIndexes = nil;

@implementation LLBasicModel

- (void)dealloc {
 
    [_primaryKeys release];
    [super dealloc];
}

+ (void)initialize {
    NSLog(@"initialize::className = %@", NSStringFromClass([self class]));
}

+ (void)load {
    
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        
        // initialize all class static objects,
        @autoreleasepool {
            allowedJSONTypes = [@[
                                [NSString class], [NSNumber class], [NSArray class], [NSDictionary class], [NSNull class], //immutable JSON classes
                                [NSMutableString class], [NSMutableArray class], [NSMutableDictionary class] //mutable JSON classes
                                ] retain];
            
            allowedPrimitiveTypes = [@[
                                     @"BOOL", @"float", @"int", @"long", @"double", @"short",
                                     ] retain];
            
            classProperties = [[NSMutableDictionary alloc] init];
            classRequiredPropertyNames = [[NSMutableDictionary alloc] init];
            classIndexes = [[NSMutableDictionary alloc] init];
            valueTransformer = [[JSONValueTransformer alloc] init];
            
        }
    });
}


- (void)setup {
    
    if (!classProperties[self.className]) {
        [self restrospectProperties];
    }
}

- (id)init
{
    self = [super init];
    if (self) {
        [self setup];
    }
    return self;
}

- (NSArray *)properties {
    
    if (classProperties[self.className]) return [classProperties[self.className] allValues];
    
    if (!self.className) [self setup];
    [self restrospectProperties];
    return [classProperties[self.className] allValues];
}

- (NSMutableSet *)requiredPropertyNames {
    
    if (!classRequiredPropertyNames[self.className]) {
        classRequiredPropertyNames[self.className] = [NSMutableSet set];
        [[self properties] enumerateObjectsUsingBlock:^(LLModelClassProperty* p, NSUInteger idx, BOOL *stop) {
            if (!p.isOptional) [classRequiredPropertyNames[self.className] addObject:p.name];
        }];
    }
    return classRequiredPropertyNames[self.className];
}


+ (NSArray *)getClassProperty:(Class)zclass {
    
    if ([zclass isSubclassOfClass:[LLBasicModel class]]) {
        
        NSString *className = NSStringFromClass(zclass);
        if (classProperties[className]) {
            
            return [classProperties[className] allValues];
        } else {
            
            NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary];
            
            //temp variables for the loops
            Class class = zclass;
            NSScanner* scanner = nil;
            NSString* propertyType = nil;
            
            // retrospect inherited properties up to the JSONModel class
            while (class != [LLBasicModel class]) {
                
                unsigned int propertyCount;
                objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
                
                //loop over the class properties
                for (int i = 0; i < propertyCount; i++) {
                    
                    LLModelClassProperty* p = [[[LLModelClassProperty alloc] init] autorelease];
                    
                    //get property name
                    objc_property_t property = properties[i];
                    const char *propertyName = property_getName(property);
                    p.name = [NSString stringWithUTF8String:propertyName];
                    
                    //get property attributes
                    const char *attrs = property_getAttributes(property);
                    
                    scanner = [NSScanner scannerWithString:
                               [NSString stringWithUTF8String:attrs]
                               ];
                    
                    [scanner scanUpToString:@"T" intoString: nil];
                    [scanner scanString:@"T" intoString:nil];
                    
                    //check if the property is an instance of a class
                    if ([scanner scanString:@"@\"" intoString: &propertyType]) {
                        
                        [scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
                                            intoString:&propertyType];
                        
                        p.type = NSClassFromString(propertyType);
                        p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound);
                        p.isStandardJSONType = [allowedJSONTypes containsObject:p.type];
                        
                        //read through the property protocols
                        while ([scanner scanString:@"<" intoString:NULL]) {
                            
                            NSString* protocolName = nil;
                            
                            [scanner scanUpToString:@">" intoString: &protocolName];
                            
                            if ([protocolName isEqualToString:@"Optional"]) {
                                p.isOptional = YES;
                            } else if([protocolName isEqualToString:@"Index"]) {
                                classIndexes[self.className] = p.name;
                            } else if([protocolName isEqualToString:@"ConvertOnDemand"]) {
                                p.convertsOnDemand = YES;
                            } else {
                                p.protocol = protocolName;
                            }
                            
                            [scanner scanString:@">" intoString:NULL];
                        }
                        
                    } else {
                        
                        //the property contains a primitive data type
                        [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","]
                                                intoString:&propertyType];
                        
                        //get the full name of the primitive type
                        propertyType = valueTransformer.primitivesNames[propertyType];
                        
                        if (![allowedPrimitiveTypes containsObject:propertyType]) {
                            
                            NSLog(@"%@", [NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", NSStringFromClass(zclass), p.name]);
                        }
                        
                    }
                    
                    //add the property object to the temp index
                    [propertyIndex setValue:p forKey:p.name];
                }
                
                free(properties);
                
                //ascend to the super of the class
                //(will do that until it reaches the root class - JSONModel)
                class = [class superclass];
            }
            
            //finally store the property index in the static property index
            classProperties[self.className] = propertyIndex;
            
            return [propertyIndex allValues];
        }
    } else {
        
        return nil;
    }
}

- (void)restrospectProperties {
    
    NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary];
    
    //temp variables for the loops
    Class class = [self class];
    NSScanner* scanner = nil;
    NSString* propertyType = nil;
    
    // retrospect inherited properties up to the JSONModel class
    while (class != [LLBasicModel class]) {
        
        unsigned int propertyCount;
        objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
        
        //loop over the class properties
        for (int i = 0; i < propertyCount; i++) {
            
            LLModelClassProperty* p = [[[LLModelClassProperty alloc] init] autorelease];
            
            //get property name
            objc_property_t property = properties[i];
            const char *propertyName = property_getName(property);
            p.name = [NSString stringWithUTF8String:propertyName];
            NSLog(@"p.name::%@",p.name);
            //get property attributes
            const char *attrs = property_getAttributes(property);
            
            scanner = [NSScanner scannerWithString:
                       [NSString stringWithUTF8String:attrs]
                       ];
            
            [scanner scanUpToString:@"T" intoString: nil];
            [scanner scanString:@"T" intoString:nil];
            
            //check if the property is an instance of a class
            if ([scanner scanString:@"@\"" intoString: &propertyType]) {
                
                [scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
                                    intoString:&propertyType];
                
                p.type = NSClassFromString(propertyType);
                p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound);
                p.isStandardJSONType = [allowedJSONTypes containsObject:p.type];
                
                //read through the property protocols
                while ([scanner scanString:@"<" intoString:NULL]) {
                    
                    NSString* protocolName = nil;
                    
                    [scanner scanUpToString:@">" intoString: &protocolName];
                    
                    if ([protocolName isEqualToString:@"Optional"]) {
                        p.isOptional = YES;
                    } else if([protocolName isEqualToString:@"Index"]) {
                        classIndexes[self.className] = p.name;
                    } else if([protocolName isEqualToString:@"ConvertOnDemand"]) {
                        p.convertsOnDemand = YES;
                    } else {
                        p.protocol = protocolName;
                    }
                    
                    [scanner scanString:@">" intoString:NULL];
                }
                
            } else {
                
                //the property contains a primitive data type
                [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","]
                                        intoString:&propertyType];
                
                //get the full name of the primitive type
                propertyType = valueTransformer.primitivesNames[propertyType];
                
                if (![allowedPrimitiveTypes containsObject:propertyType]) {
                    
                    NSLog(@"%@", [NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", self.className, p.name]);
                }
                
            }
            
            //add the property object to the temp index
            [propertyIndex setValue:p forKey:p.name];
        }
        
        free(properties);
        
        //ascend to the super of the class
        //(will do that until it reaches the root class - JSONModel)
        class = [class superclass];
    }
    
    //finally store the property index in the static property index
    classProperties[self.className] = propertyIndex;
}

- (NSString *)description {
    
    NSMutableString* text = [NSMutableString stringWithFormat:@"<%@> \n", NSStringFromClass([self class])];
    NSArray* properties = [self properties];
    
    for (int i=0;i<properties.count;i++) {
        
        LLModelClassProperty* p = (LLModelClassProperty*)properties[i];
        
        id value = [self valueForKey:p.name];
        NSString* valueDescription = (value)?[value description]:@"<nil>";
        
        if (p.isStandardJSONType && [valueDescription length]>60 && !p.convertsOnDemand) {
            
            //cap description for longer values
            valueDescription = [NSString stringWithFormat:@"%@...", [valueDescription substringToIndex:59]];
        }
        valueDescription = [valueDescription stringByReplacingOccurrencesOfString:@"\n" withString:@"\n   "];
        [text appendFormat:@"   [%@]: %@\n", p.name, valueDescription];
    }
    
    [text appendFormat:@"</%@>", NSStringFromClass([self class])];
    return text;
}

#pragma mark - Dict And Object
- (id)initWithDictionary:(NSDictionary *)dict {
    
    self = [self init];
    BOOL success = NO;
    if(self) {
        success = [self dictionaryForObject:dict];
    }
    
    if (success) {
        return self;
    }else{
        [self release];
        return nil;
    }    
}

- (BOOL)dictionaryForObject:(NSDictionary*)dict{
    
    NSArray* incomingKeysArray = [dict allKeys];
    NSMutableSet* requiredProperties = [NSMutableSet setWithSet:[self requiredPropertyNames]];
    NSMutableSet* incomingKeys = [NSMutableSet setWithArray:incomingKeysArray];
    
    //M_yzf20131209:允许后台新增字段
    //check for missing input keys
    if (![incomingKeys isSubsetOfSet:requiredProperties]) {
        
        //get a list of the missing properties
//        [incomingKeys minusSet:requiredProperties];
        //记录后台增加的字段
        NSMutableSet *tempSet = [NSMutableSet setWithSet:incomingKeys];
        [tempSet minusSet:requiredProperties];
        
        //not all required properties are in - invalid input
        NSLog(@"Incoming data was invalid [%@ initWithDictionary:]. Keys missing: %@", self.className, tempSet);
//        return NO;
    }
    
    incomingKeys = nil;
    requiredProperties = nil;
    
    
    for (LLModelClassProperty* property in [self properties]) {
        
        //convert key name ot model keys, if a mapper is provided
        NSString* jsonKeyPath = property.name;
        
        //general check for data type compliance
        id jsonValue = [dict valueForKeyPath: jsonKeyPath];
        
        //check for Optional properties
        if (jsonValue==nil) {
            
            //skip this property, continue with next property
            continue;
        }
        
        Class jsonValueClass = [jsonValue class];
        BOOL isValueOfAllowedType = NO;
        
        for (Class allowedType in allowedJSONTypes) {
            if ( [jsonValueClass isSubclassOfClass: allowedType] ) {
                isValueOfAllowedType = YES;
                break;
            }
        }
        
        if (isValueOfAllowedType == NO) {
            //type not allowed
            NSLog(@"Type %@ is not allowed in JSON.", NSStringFromClass(jsonValueClass));
            return NO;
        }
        
        //check if there's matching property in the model
        
        if (property) {
            
            // 0) handle primitives
            if (property.type == nil) {
                
                //just copy the value
                [self setValue:jsonValue forKey: property.name];
                //skip directly to the next key
                continue;
            }
            
            // 0.5) handle nils
            if (isNull(jsonValue)) {
                [self setValue:nil forKey: property.name];
                continue;
            } else if (isNullDict(jsonValue)) {
                [self setValue:[NSDictionary dictionary] forKey: property.name];
                continue;
            } else if (isNullArray(jsonValue)) {
                [self setValue:[NSArray array] forKey: property.name];
                continue;
            }
            
            // 1) check if property is itself a LLBasicModel
            if ([[property.type class] isSubclassOfClass:[LLBasicModel class]]) {
                
                //initialize the property's model, store it
                id value = [[property.type alloc] initWithDictionary:jsonValue];
                
                if (!value) {
                    NSLog(@"init %@ class fail", property.type);
                    return NO;
                }
                [self setValue:value forKey: property.name];
                [value release];
                continue;
                
            } else {
                
                // 2) check if there's a protocol to the property
                //  ) might or not be the case there's a built in transofrm for it
                if (property.protocol) {
                    
                    //JMLog(@"proto: %@", p.protocol);
                    jsonValue = [self transform:jsonValue forProperty:property];
                    if (!jsonValue) {                        
                        return NO;
                    }
                }
                
                // 3.1) handle matching standard JSON types
                if (property.isStandardJSONType && [jsonValue isKindOfClass: property.type]) {
                    
                    
                    //mutable properties
                    if (property.isMutable) {
                        jsonValue = [jsonValue mutableCopy];
                    }
                    
                    //set the property value
                    [self setValue:jsonValue forKey: property.name];
                    
                    continue;
                }
                
                // 3.3) handle values to transform
                if ((![jsonValue isKindOfClass:property.type] && !isNull(jsonValue))|| property.isMutable) {
                    
                    // searched around the web how to do this better
                    // but did not find any solution, maybe that's the best idea? (hardly)
                    Class sourceClass = [JSONValueTransformer classByResolvingClusterClasses:[jsonValue class]];
                                        
                    //build a method selector for the property and json object classes
                    NSString* selectorName = [NSString stringWithFormat:@"%@From%@:", property.type, sourceClass];
                    SEL selector = NSSelectorFromString(selectorName);
                    
                    //check if there's a transformer with that name
                    if ([valueTransformer respondsToSelector:selector]) {
                        
                        //it's OK, believe me...
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                        //transform the value
                        jsonValue = [valueTransformer performSelector:selector withObject:jsonValue];
#pragma clang diagnostic pop
                        
                        [self setValue:jsonValue forKey: property.name];
                        
                    } else {
                        
                        // it's not a JSON data type, and there's no transformer for it
                        // if property type is not supported - that's a programmer mistaked -> exception
                        @throw [NSException exceptionWithName:@"Type not allowed"
                                                       reason:[NSString stringWithFormat:@"%@ type not supported for %@.%@", property.type, [self class], property.name]
                                                     userInfo:nil];
                        return NO;
                    }
                    
                } else {
                    // 3.4) handle "all other" cases (if any)
                    [self setValue:jsonValue forKey: property.name];
                }
            }
        }
    }
    return YES;
}

- (id)transform:(id)value forProperty:(LLModelClassProperty *)property {
    Class protocolClass = NSClassFromString(property.protocol);
    if (!protocolClass) {
        
        //no other protocols on arrays and dictionaries
        //except JSONModel classes
        if ([value isKindOfClass:[NSArray class]]) {
            @throw [NSException exceptionWithName:@"Bad property protocol declaration"
                                           reason:[NSString stringWithFormat:@"<%@> is not allowed JSONModel property protocol, and not a JSONModel class.", property.protocol]
                                         userInfo:nil];
        }
        return value;
    }
    
    //if the protocol is actually a LLBasicModel class
    if ([[protocolClass class] isSubclassOfClass:[LLBasicModel class]]) {
        
        //check if it's a list of models
        if ([property.type isSubclassOfClass:[NSArray class]]) {
            
            if (property.convertsOnDemand) {
                //on demand conversion
//                value = [[[JSONModelArray alloc] initWithArray:value modelClass:[protocolClass class]] autorelease];
                
            } else {
                //one shot conversion
                value = [[protocolClass class] arrayOfModelsFromDictionaries: value];
            }
        }
        
        //check if it's a dictionary of models
        if ([property.type isSubclassOfClass:[NSDictionary class]]) {
            
            NSMutableDictionary* res = [NSMutableDictionary dictionary];
            
            for (NSString* key in [value allKeys]) {
                id obj = [[[[protocolClass class] alloc] initWithDictionary:value[key]] autorelease];
                if (obj) {
                    [res setValue:obj forKey:key];
                } else {
                    NSLog(@"dict create class failed...");
                    return nil;
                }
            }
            value = [NSDictionary dictionaryWithDictionary:res];
        }
    }
    
    return value;
}

+ (NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array {
    
    //bail early
    if (isNull(array)) return nil;
    
    //parse dictionaries to objects
    NSMutableArray* list = [NSMutableArray arrayWithCapacity:[array count]];
    
    for (NSDictionary* d in array) {
        
        id obj = [[[self alloc] initWithDictionary:d] autorelease];
        if (!obj) return nil;
        
        [list addObject: obj];
    }
    
    return list;
}

- (void)createDictionariesForKeyPath:(NSString*)keyPath inDictionary:(NSMutableDictionary**)dict {
    
    //find if there's a dot left in the keyPath
    NSUInteger dotLocation = [keyPath rangeOfString:@"."].location;
    if (dotLocation==NSNotFound) return;
    
    //inspect next level
    NSString* nextHierarchyLevelKeyName = [keyPath substringToIndex: dotLocation];
    NSMutableDictionary* nextLevelDictionary = [*dict objectForKey:nextHierarchyLevelKeyName];
    
    if (nextLevelDictionary==nil) {
        //create non-existing next level here
        nextLevelDictionary = [NSMutableDictionary dictionary];
    }
    
    //recurse levels
    [self createDictionariesForKeyPath:[keyPath substringFromIndex:dotLocation+1]
                            inDictionary:&nextLevelDictionary];
    
    //create the hierarchy level
    [*dict setValue:nextLevelDictionary  forKeyPath: nextHierarchyLevelKeyName];
}

- (id)reverseTransform:(id)value forProperty:(LLModelClassProperty*)property
{
    Class protocolClass = NSClassFromString(property.protocol);
    if (!protocolClass) return value;
    
    //if the protocol is actually a JSONModel class
    if ([[protocolClass class] isSubclassOfClass:[LLBasicModel class]]) {
        
        //check if should export list of dictionaries
        if (property.type == [NSArray class]) {
            NSMutableArray* tempArray = [NSMutableArray arrayWithCapacity: [(NSArray*)value count] ];
            for (id<AbstractJSONModelProtocol> model in (NSArray*)value) {
                [tempArray addObject: [model toDictionary] ];
            }
            return [NSArray arrayWithArray: tempArray];
        }
        
        //check if should export dictionary of dictionaries
        if (property.type == [NSDictionary class]) {
            NSMutableDictionary* res = [NSMutableDictionary dictionary];
            for (NSString* key in [(NSDictionary*)value allKeys]) {
                id<AbstractJSONModelProtocol> model = value[key];
                [res setValue: [model toDictionary] forKey: key];
            }
            return [NSDictionary dictionaryWithDictionary:res];
        }
    }
    
    return value;
}

-(NSDictionary*)toDictionary
{
    NSArray* properties = [self properties];
    NSMutableDictionary* tempDictionary = [NSMutableDictionary dictionaryWithCapacity:properties.count];
    
    id value;
        
    //loop over all properties
    for (LLModelClassProperty* p in properties) {
        
        //fetch key and value
        NSString* keyPath = p.name;
        value = [self valueForKey: p.name];
        
        if ([keyPath rangeOfString:@"."].location != NSNotFound) {
            //there are sub-keys, introduce dictionaries for them
            [self createDictionariesForKeyPath:keyPath inDictionary:&tempDictionary];
        }
        
        //export nil values as JSON null, so that the structure of the exported data
        //is still valid if it's to be imported as a model again
        if (isNull(value)) {
            
            [tempDictionary setValue:[NSNull null] forKeyPath:keyPath];
            continue;
        }
        
        //check if the property is another model
        if ([value isKindOfClass:[LLBasicModel class]]) {
            
            //recurse models
            value = [(LLBasicModel*)value toDictionary];
            [tempDictionary setValue:value forKeyPath: keyPath];
            
            //for clarity
            continue;
            
        } else {
            
            // 1) check for built-in transformation
            if (p.protocol) {
                value = [self reverseTransform:value forProperty:p];
            }
            
            // 2) check for standard types OR 2.1) primitives
            if (p.isStandardJSONType || p.type==nil) {
                [tempDictionary setValue:value forKeyPath: keyPath];
                continue;
            }
            
            // 3) try to apply a value transformer
            if (YES) {
                
                [tempDictionary setValue:value forKeyPath: keyPath];                
                
            }
            
        }
        
    }
    
    return [NSDictionary dictionaryWithDictionary: tempDictionary];
}

#pragma mark - JSON And Object
- (id)initWithJSONString:(NSString *)jsonString {
    
    self = [self init];
    if(self)
        [self JSONForObject:jsonString];
    return self;
    
}

- (void)JSONForObject:(NSString *)jsonString {
    
    NSDictionary *dict = [jsonString objectFromJSONString];
    
    if (dict == nil) {
//        NSLog(@"create %@ modle failed", [self className]);
    } else if (![dict isKindOfClass:[NSDictionary class]]) {
        assert(@"please give modle a dict type");
    } else {
        [self dictionaryForObject:dict];        
    }
}

- (NSString *)toJSONString {
    
    NSDictionary *dict = [self toDictionary];
    if (dict) {
        return [dict JSONString];
    } else {
//        DDLogWarn(@"no values for this modle");
        return nil;
    }
}

- (NSData *)toJSONData {
    
    NSDictionary *dict = [self toDictionary];
    if (dict) {
        return [dict JSONData];
    } else {
        //DDLogWarn(@"no values for this modle");
        return nil;
    }
}

@end
